package com.sky.aspect;

import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;

import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * 自定义切面（切面）类：用于公共字段自动段自动填充的处理逻辑
 * 作用：通过 AOP 机制，在执行数据库操作前自动为实体类的公共字段（如创建时间、更新人等）赋值
 */
@Aspect  // 标记当前类是一个切面类，用于定义切入点和通知
@Component  // 将当前类注册到 Spring 容器中，使其被 Spring 管理
@Slf4j  // 借助 lombok 生成日志对象，可直接使用 log 打印日志
public class AutoFillAspect {

    /**
     * 定义切入点：指定哪些方法会被当前切面拦截
     */
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}  // 切入点方法（方法体为空，仅作为标记）
    // 切入点表达式说明：
    // 1. execution(* com.sky.mapper.*.*(..))：匹配 com.sky.mapper 包下所有类的所有方法
    // 2. @annotation(com.sky.annotation.AutoFill)：匹配添加了 @AutoFill 注解的方法
    // 3. &&：两个条件同时满足时才会被拦截


    /**
     * 前置通知：在被拦截的方法执行之前执行以下逻辑
     * 作用：在数据库操作（新增/修改）前，自动为公共字段赋值
     */
    @Before("autoFillPointCut()")  // 指定当前通知关联上面定义的切入点
    public void autoFill(JoinPoint joinPoint){  // JoinPoint：包含被拦截方法的详细信息
        log.info("开始进行公共字段的自动填充...");  // 打印日志，标记进入自动填充流程


        // 第一步：获取当前操作的类型（是新增还是修改）
        // 1. 通过 joinPoint 获取被拦截方法的签名（包含方法的元数据）
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        // 2. 从方法签名中获取被拦截的方法对象
        // 3. 从方法对象上获取 @AutoFill 注解
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
        // 4. 从注解中获取操作类型（INSERT 或 UPDATE）
        OperationType operationType = autoFill.value();


        // 第二步：获取需要赋值的实体对象（被拦截方法的参数）
        // 1. 获取被拦截方法的所有参数（参数列表）
        Object[] args = joinPoint.getArgs();
        // 2. 校验参数是否存在（如果没有参数，直接返回，无需填充）
        if (args == null || args.length == 0){
            return;
        }
        // 3. 假设方法的第一个参数就是需要操作的实体对象（如 User、Order 等）
        Object entity = args[0];


        // 第三步：准备需要填充的公共字段值
        LocalDateTime now = LocalDateTime.now();  // 当前时间（用于创建时间/更新时间）
        Long currentId = BaseContext.getCurrentId();  // 当前登录用户的 ID（从上下文获取，用于创建人/更新人）


        // 第四步：根据操作类型，通过反射为实体对象的对应字段赋值
        if (operationType == OperationType.INSERT){  // 如果是新增操作
            // 需要为 4 个字段赋值：创建时间、创建人、更新时间、更新人
            try {
                // 1. 通过反射获取实体类中对应字段的 set 方法
                // AutoFillConstant.SET_CREATE_TIME 是常量，值为 "setCreateTime"（方法名）
                Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
                Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                // 2. 调用 set 方法为字段赋值（参数：实体对象、要赋的值）
                setCreateTime.invoke(entity, now);  // 设置创建时间为当前时间
                setCreateUser.invoke(entity, currentId);  // 设置创建人为当前登录用户
                setUpdateTime.invoke(entity, now);  // 设置更新时间为当前时间
                setUpdateUser.invoke(entity, currentId);  // 设置更新人为当前登录用户
            } catch (Exception e) {
                e.printStackTrace();  // 如果反射过程出错，打印异常信息
            }
        } else if (operationType == OperationType.UPDATE){  // 如果是修改操作
            // 需要为 2 个字段赋值：更新时间、更新人
            try {
                // 1. 通过反射获取实体类中对应字段的 set 方法
                Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
                Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);

                // 2. 调用 set 方法为字段赋值
                setUpdateTime.invoke(entity, now);  // 设置更新时间为当前时间
                setUpdateUser.invoke(entity, currentId);  // 设置更新人为当前登录用户
            } catch (Exception e) {
                e.printStackTrace();  // 如果反射过程出错，打印异常信息
            }
        }

    }

}




